Skip to content

feat: add LLM provider pricing info dialog - #3484

Closed
JanCizmar wants to merge 2 commits into
mainfrom
JanCizmar/map-based-llm-config
Closed

feat: add LLM provider pricing info dialog#3484
JanCizmar wants to merge 2 commits into
mainfrom
JanCizmar/map-based-llm-config

Conversation

@JanCizmar

@JanCizmar JanCizmar commented Feb 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Extend LlmProviderSimpleModel with tokenPriceInCreditsInput and tokenPriceInCreditsOutput fields
  • Create pricing info dialog showing token prices, estimated costs for 1,000 strings (with/without screenshots), and info notes about margin and plan credits
  • Show info icon on server provider rows only when billing is enabled
  • Fetch subscription data to convert credit prices to USD
  • Add E2E test verifying pricing info is hidden when billing is disabled

Test plan

  • E2E test: pricing info icon not shown when billing is disabled
  • Manual: with billing enabled, click info icon on server provider — verify pricing table shows
  • Manual: verify USD column appears when subscription has perThousandMtCredits
  • Manual: verify "no pricing" message when provider has no token prices configured

Summary by CodeRabbit

  • New Features

    • Added LLM provider pricing display showing input and output token prices in credits
    • Added pricing information dialog with detailed cost calculations and per-token estimates
    • Made pricing UI billing-aware—pricing information only displays when billing is enabled
  • Tests

    • Added comprehensive unit test suite for provider configuration merging
    • Added e2e test validating pricing information visibility based on billing state

Enable separating non-secret LLM provider config (model, prices, type)
from secrets (API keys) in Kubernetes deployments via a new
`provider-defaults` map that merges with the existing `providers` list.
Add a pricing information dialog for server-configured LLM providers.
Organization owners can now see token pricing in credits and estimated
costs for translating 1,000 strings.

- Extend LlmProviderSimpleModel with tokenPriceInCreditsInput/Output
- Create LlmProviderPricingDialog with token prices table, cost
  estimates (with/without screenshots), and info notes
- Create LlmProviderPricingInfo as self-contained icon + dialog
- Only show pricing info when billing is enabled
- Fetch subscription data to convert credits to USD
- Add E2E test verifying pricing info is hidden without billing
@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds token pricing support for LLM providers. It introduces provider-level token price defaults in configuration, implements merge logic in the service layer to combine defaults with explicit provider configurations, exposes pricing data through API models, and adds a React UI component that conditionally displays pricing information when billing is enabled.

Changes

Cohort / File(s) Summary
LLM Token Pricing Model
backend/api/src/main/kotlin/io/tolgee/hateoas/llmProvider/LlmProviderSimpleModel.kt, ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/LlmProviderSimpleModelAssembler.kt
Added tokenPriceInCreditsInput and tokenPriceInCreditsOutput nullable Double properties to the model and updated the assembler to populate these fields from source data.
LLM Provider Configuration & Defaults
backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/LlmProperties.kt
Introduced providerDefaults map and new LlmProviderDefaults class to hold per-provider default configurations, with fields for pricing, max tokens, and other provider settings, including a toLlmProvider() conversion helper.
Provider Merge Service Logic
backend/data/src/main/kotlin/io/tolgee/service/LlmPropertiesService.kt
Added getMergedProviders() function to compute merged provider entries by aligning list-based providers with map defaults, applying overrides where applicable, and ensuring maxTokens defaults are applied correctly.
Backend Tests
backend/data/src/test/kotlin/io/tolgee/unit/LlmPropertiesServiceMergeTest.kt, e2e/cypress/e2e/llmProviders/llmProviders.cy.ts
Added comprehensive Kotlin unit test suite for merging logic covering defaults, list overrides, null field handling, and disabled providers; added e2e test verifying pricing info is hidden when billing is disabled.
Pricing UI Components
webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingDialog.tsx, webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx, webapp/src/ee/llm/OrganizationLLMProviders/LlmProvidersServer.tsx
Added new LlmProviderPricingDialog and LlmProviderPricingInfo components for displaying provider pricing details; updated LlmProvidersServer to conditionally render pricing info based on billing state and adjust layout accordingly.
Type Definitions
e2e/cypress/support/dataCyType.d.ts
Extended DataCy.Value union type to include new "llm-provider-pricing-info" data-cy selector value.

Sequence Diagram(s)

sequenceDiagram
    participant Config as Config Layer
    participant Service as LlmPropertiesService
    participant API as API Assembler
    participant Frontend as React UI
    
    Config->>Service: providerDefaults + providers list
    Service->>Service: getMergedProviders()
    Note over Service: Merge map defaults with list entries<br/>Apply overrides & defaults
    Service->>API: Merged LlmProvider objects
    API->>API: Map to LlmProviderSimpleModel<br/>Include token prices
    API->>Frontend: JSON with pricing data
    Frontend->>Frontend: Check billing.enabled
    alt Billing Enabled
        Frontend->>Frontend: Render LlmProviderPricingInfo
        Note over Frontend: Display pricing button & dialog
    else Billing Disabled
        Frontend->>Frontend: Skip pricing component
        Note over Frontend: No pricing info shown
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • dkrizan
  • Anty0

Poem

🐰 Token prices now flow through the system bright,
Defaults and merges combine just right,
From config to UI, the pricing takes flight,
With billing-aware billing cards shown in sight! 💰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature addition: a pricing info dialog for LLM providers, which is a primary focus of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch JanCizmar/map-based-llm-config

Comment @coderabbitai help to get the list of available commands and usage tips.

@JanCizmar
JanCizmar marked this pull request as draft February 20, 2026 16:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/cypress/e2e/llmProviders/llmProviders.cy.ts`:
- Around line 103-107: The test "does not show pricing info when billing is
disabled" currently uses text-based selection
gcy('organization-llm-providers-tab').contains('Server').click(); change it to
target a dedicated data-cy for the server tab (e.g.
gcy('organization-llm-providers-server-tab').click()) and update the
DOM/component to include that data-cy if missing; keep the rest of the
assertions (gcy('llm-provider-item-name') and gcy('llm-provider-pricing-info'))
unchanged so the test uses only data-cy selectors per guidelines.

In `@webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx`:
- Around line 38-44: The IconButton that opens the pricing info modal is
icon-only and lacks an accessible name; update the IconButton (the component
rendering InfoCircle and calling setOpen) to include an aria-label prop using
the existing translation key used for this tooltip/label in this module (i.e.,
add aria-label={t('...existingKey...')} or the specific translation constant
already present in this file) so screen readers get a proper accessible name.
- Around line 1-5: The relative import of LlmProviderPricingDialog should use
the project's tg.* alias; update the import statement that currently references
'./LlmProviderPricingDialog' in LlmProviderPricingInfo (the file importing
LlmProviderPricingDialog) to use
'tg.ee.module/llm/OrganizationLLMProviders/LlmProviderPricingDialog' so the
module resolver uses the Tolgee path alias.

Comment on lines +103 to +107
it('does not show pricing info when billing is disabled', () => {
gcy('organization-llm-providers-tab').contains('Server').click();
gcy('llm-provider-item-name').should('contain', 'server-provider');
gcy('llm-provider-pricing-info').should('not.exist');
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid text-based selection for the “Server” tab.
Use a dedicated data-cy on the server tab and target it via gcy(...), instead of .contains('Server').

🛠️ Proposed adjustment
-    gcy('organization-llm-providers-tab').contains('Server').click();
+    gcy('organization-llm-providers-tab-server').click();

As per coding guidelines: **/*.{tsx,ts}: STRICTLY use data-cy attributes for E2E selectors, never rely on text content; use typed helpers gcy() or cy.gcy().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/cypress/e2e/llmProviders/llmProviders.cy.ts` around lines 103 - 107, The
test "does not show pricing info when billing is disabled" currently uses
text-based selection
gcy('organization-llm-providers-tab').contains('Server').click(); change it to
target a dedicated data-cy for the server tab (e.g.
gcy('organization-llm-providers-server-tab').click()) and update the
DOM/component to include that data-cy if missing; keep the rest of the
assertions (gcy('llm-provider-item-name') and gcy('llm-provider-pricing-info'))
unchanged so the test uses only data-cy selectors per guidelines.

Comment on lines +1 to +5
import { Box, IconButton, styled, Tooltip } from '@mui/material';
import { useState } from 'react';
import { InfoCircle } from '@untitled-ui/icons-react';
import { useTranslate } from '@tolgee/react';
import { LlmProviderPricingDialog } from './LlmProviderPricingDialog';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find tsconfig files with path alias configuration
echo "=== Looking for tsconfig files and path alias config ==="
find . -maxdepth 3 -name "tsconfig*.json" -type f | head -20

echo ""
echo "=== Checking webapp tsconfig for tg.* aliases ==="
cat webapp/tsconfig.json 2>/dev/null | jq '.compilerOptions.paths' 2>/dev/null || cat webapp/tsconfig.json 2>/dev/null | grep -A 20 '"paths"'

echo ""
echo "=== Finding existing tg.* import usage in webapp/src/ee/llm ==="
rg 'from\s+["\']tg\.(component|service|hooks|views|globalContext)' webapp/src/ee/llm -A 1 -B 1 || echo "No tg.* imports found in ee/llm"

echo ""
echo "=== Finding all imports in the target file ==="
cat -n webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx | head -10

echo ""
echo "=== Checking what LlmProviderPricingDialog exports ==="
cat -n webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingDialog.tsx | head -15

Repository: tolgee/tolgee-platform

Length of output: 717


🏁 Script executed:

#!/bin/bash

echo "=== Direct inspection of webapp/tsconfig.json ==="
cat webapp/tsconfig.json

echo ""
echo "=== Finding tg.* imports in webapp codebase ==="
rg 'from ["\047]tg\.' webapp/src -l --max-count 5

echo ""
echo "=== Actual target file imports ==="
head -10 webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx

echo ""
echo "=== Check if LlmProviderPricingDialog.tsx is a component ==="
head -20 webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingDialog.tsx

echo ""
echo "=== Find other imports in same directory ==="
rg 'from ["\047]\.' webapp/src/ee/llm/OrganizationLLMProviders/ -c | head -20

Repository: tolgee/tolgee-platform

Length of output: 2250


🏁 Script executed:

#!/bin/bash

echo "=== Checking tsconfig.extend.json for path aliases ==="
cat webapp/tsconfig.extend.json

echo ""
echo "=== Simple search for any 'tg.component' usage in webapp ==="
rg 'tg\.component' webapp/src --max-count 3

echo ""
echo "=== Simple search for any 'tg.' imports ==="
rg "tg\." webapp/src --max-count 3

echo ""
echo "=== Check vite config for path aliases ==="
fd 'vite.config' webapp/ -x cat {}

Repository: tolgee/tolgee-platform

Length of output: 50379


Replace the relative import with a Tolgee path alias.

This import should use the repo's tg.* aliases. Change from './LlmProviderPricingDialog' to from 'tg.ee.module/llm/OrganizationLLMProviders/LlmProviderPricingDialog'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx` around
lines 1 - 5, The relative import of LlmProviderPricingDialog should use the
project's tg.* alias; update the import statement that currently references
'./LlmProviderPricingDialog' in LlmProviderPricingInfo (the file importing
LlmProviderPricingDialog) to use
'tg.ee.module/llm/OrganizationLLMProviders/LlmProviderPricingDialog' so the
module resolver uses the Tolgee path alias.

Comment on lines +38 to +44
<IconButton
size="small"
color="inherit"
onClick={() => setOpen(true)}
>
<InfoCircle />
</IconButton>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read the content around lines 38-44
head -50 webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx | cat -n

Repository: tolgee/tolgee-platform

Length of output: 1772


🏁 Script executed:

# Also search for the translation key mentioned in the suggestion
rg "llm_provider_pricing_dialog_title" webapp/

Repository: tolgee/tolgee-platform

Length of output: 339


🏁 Script executed:

# Check for other IconButton uses in the codebase to see if aria-label is a common pattern
rg -n "IconButton" webapp/src --type ts --type tsx -C2 | grep -A2 -B2 "aria-label" | head -30

Repository: tolgee/tolgee-platform

Length of output: 93


🏁 Script executed:

# Search for IconButton with aria-label to see if it's a pattern
rg "IconButton" webapp/src -A3 -B1 | grep -A3 -B1 "aria-label" | head -40

Repository: tolgee/tolgee-platform

Length of output: 1752


🏁 Script executed:

# Also check if there are other icon-only buttons in similar contexts
rg "<InfoCircle" webapp/src -B5 -A5 | head -50

Repository: tolgee/tolgee-platform

Length of output: 3903


🏁 Script executed:

# Check guidelines or accessibility patterns in the codebase
fd "accessibility\|a11y\|aria" webapp/src --type f

Repository: tolgee/tolgee-platform

Length of output: 48


Add an accessible label to the icon-only button.

Icon-only IconButton needs an accessible name; the tooltip is not a reliable screen-reader label. Add aria-label with the existing translation key.

🛠️ Suggested fix
           <IconButton
             size="small"
             color="inherit"
+            aria-label={t('llm_provider_pricing_dialog_title')}
             onClick={() => setOpen(true)}
           >
             <InfoCircle />
           </IconButton>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<IconButton
size="small"
color="inherit"
onClick={() => setOpen(true)}
>
<InfoCircle />
</IconButton>
<IconButton
size="small"
color="inherit"
aria-label={t('llm_provider_pricing_dialog_title')}
onClick={() => setOpen(true)}
>
<InfoCircle />
</IconButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx` around
lines 38 - 44, The IconButton that opens the pricing info modal is icon-only and
lacks an accessible name; update the IconButton (the component rendering
InfoCircle and calling setOpen) to include an aria-label prop using the existing
translation key used for this tooltip/label in this module (i.e., add
aria-label={t('...existingKey...')} or the specific translation constant already
present in this file) so screen readers get a proper accessible name.

@JanCizmar JanCizmar closed this Feb 26, 2026
@JanCizmar
JanCizmar deleted the JanCizmar/map-based-llm-config branch February 26, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant